| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- 'use client';
- import Link from 'next/link';
- import { useRouter } from 'next/navigation';
- import { useState } from 'react';
- import { Heart, MessageCircle, Share2, BadgeCheck, MoreHorizontal, Bookmark, Flag, Link as LinkIcon, Trash2, ArrowLeft } from 'lucide-react';
- import { fetchApi } from '@/lib/utils/client';
- import useAuth from '@/hooks/useAuth';
- import { formatDate, getDateTime } from '@/lib/utils/client';
- import { FeedPostDetail, FeedReply } from '@/types/feed/post';
- import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- DropdownMenuSeparator
- } from '@/components/ui/dropdown-menu';
- import TagChip from '../../../_component/TagChip';
- import FeedLightbox from '../../../_component/FeedLightbox';
- import FeedReplyList from './FeedReplyList';
- import FeedReplyComposer from './FeedReplyComposer';
- type Props = {
- post: FeedPostDetail;
- initialReplies: FeedReply[];
- initialRepliesTotal: number;
- };
- export default function FeedPostView({ post, initialReplies, initialRepliesTotal }: Props) {
- const router = useRouter();
- const { loginCheck } = useAuth();
- const [likes, setLikes] = useState(post.likes);
- const [liked, setLiked] = useState(post.hasLike);
- const [bookmarked, setBookmarked] = useState(post.hasBookmark);
- const [busy, setBusy] = useState(false);
- const [replyTarget, setReplyTarget] = useState<FeedReply|null>(null);
- const [refreshKey, setRefreshKey] = useState<number>(0);
- const [lightboxIndex, setLightboxIndex] = useState<number|null>(null);
- const authorDisplay = post.authorName || post.authorSID || '알 수 없음';
- const avatarInitial = (authorDisplay.charAt(0) || '?').toUpperCase();
- const imageUrls = post.images.map(i => i.url);
- const handleLike = async () => {
- if (!loginCheck() || busy) {
- return;
- }
- setBusy(true);
- try {
- const res = await fetchApi<{ hasLike: boolean; likes: number }>(`/api/feed/post/${post.postID}/like`, {
- method: 'POST',
- silent: true
- });
- if (res.success && res.data) {
- setLiked(res.data.hasLike);
- setLikes(res.data.likes);
- }
- } catch (err) {
- console.error(err);
- } finally {
- setBusy(false);
- }
- };
- const handleBookmark = async () => {
- if (!loginCheck() || busy) {
- return;
- }
- setBusy(true);
- try {
- const res = await fetchApi<{ hasBookmark: boolean; bookmarks: number }>(`/api/feed/post/${post.postID}/bookmark`, {
- method: 'POST',
- silent: true
- });
- if (res.success && res.data) {
- setBookmarked(res.data.hasBookmark);
- }
- } catch (err) {
- console.error(err);
- } finally {
- setBusy(false);
- }
- };
- const handleShare = async () => {
- const url = `${window.location.origin}/feed/post/${post.postID}`;
- try {
- await navigator.clipboard.writeText(url);
- alert('링크가 복사되었습니다.');
- } catch {
- prompt('링크를 복사하세요:', url);
- }
- };
- const handleDelete = async () => {
- if (!confirm('이 게시글을 삭제할까요?')) {
- return;
- }
- try {
- const res = await fetchApi(`/api/feed/post/${post.postID}`, {
- method: 'DELETE',
- silent: true
- });
- if (res.success) {
- router.push('/feed/all');
- router.refresh();
- }
- } catch (err) {
- console.error(err);
- }
- };
- const handleReplyTarget = (target: FeedReply) => {
- setReplyTarget(target);
- };
- const handleReplySubmitted = () => {
- setRefreshKey((k) => k + 1);
- };
- const handleBack = () => {
- if (window.history.length > 1) {
- router.back();
- } else {
- router.push('/feed/all');
- }
- };
- return (
- <>
- <nav className="feed__page-header" aria-label="페이지 헤더">
- <button type="button" className="feed__page-back" onClick={handleBack} aria-label="뒤로 가기">
- <ArrowLeft size={20} />
- </button>
- <h1 className="feed__page-title">게시물</h1>
- </nav>
- <article className="feed__post">
- <header className="feed__post-header">
- {post.authorSID ? (
- <Link href={`/user/${post.authorSID}`} className="feed__post-avatar" aria-label={`${authorDisplay} 프로필`}>
- {post.authorThumb ? (
- <img src={post.authorThumb} alt={authorDisplay} />
- ) : (
- <span className="feed__post-avatar-fallback">{avatarInitial}</span>
- )}
- </Link>
- ) : (
- <span className="feed__post-avatar">
- <span className="feed__post-avatar-fallback">{avatarInitial}</span>
- </span>
- )}
- <div className="feed__post-meta">
- <div className="feed__post-author-row">
- {post.authorSID ? (
- <Link href={`/user/${post.authorSID}`} className="feed__post-author">{authorDisplay}</Link>
- ) : (
- <span className="feed__post-author">{authorDisplay}</span>
- )}
- {post.isCreator && (
- <span className="feed__post-badge" title="크리에이터">
- <BadgeCheck size={14} fill="currentColor" stroke="#fff" strokeWidth={2.5} />
- </span>
- )}
- </div>
- <div className="feed__post-meta-row">
- <span title={getDateTime(post.createdAt)}>{formatDate(post.createdAt)}</span>
- <span>· 조회 {post.views.toLocaleString()}</span>
- </div>
- </div>
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <button type="button" className="feed__post-more" aria-label="더보기">
- <MoreHorizontal size={20} />
- </button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem onClick={handleBookmark}>
- <Bookmark size={14} fill={bookmarked ? 'currentColor' : 'none'} className="mr-2" />
- {bookmarked ? '저장 해제' : '저장'}
- </DropdownMenuItem>
- <DropdownMenuItem onClick={handleShare}>
- <LinkIcon size={14} className="mr-2" />
- 링크 복사
- </DropdownMenuItem>
- {post.isOwner && (
- <>
- <DropdownMenuSeparator />
- <DropdownMenuItem onClick={handleDelete} className="text-red-600">
- <Trash2 size={14} className="mr-2" />
- 삭제
- </DropdownMenuItem>
- </>
- )}
- {!post.isOwner && (
- <DropdownMenuItem>
- <Flag size={14} className="mr-2" />
- 신고
- </DropdownMenuItem>
- )}
- </DropdownMenuContent>
- </DropdownMenu>
- </header>
- <div className="feed__post-body">
- <p className="feed__post-content">{post.content}</p>
- </div>
- {post.images.length > 0 && (
- <div className={`feed__post-gallery feed__post-gallery--count-${Math.min(post.images.length, 4)}`}>
- {post.images.slice(0, 4).map((image, index) => (
- <button
- type="button"
- key={image.url}
- className="feed__post-gallery-item"
- onClick={() => setLightboxIndex(index)}
- aria-label={`이미지 ${index + 1} 크게 보기`}
- >
- <img src={image.url} alt={`이미지 ${index + 1}`} loading="lazy" />
- {index === 3 && post.images.length > 4 && (
- <span className="feed__post-gallery-more">+{post.images.length - 4}</span>
- )}
- </button>
- ))}
- </div>
- )}
- {post.tags.length > 0 && (
- <div className="feed__post-tags">
- {post.tags.map((tag) => (
- <TagChip key={tag.slug} name={tag.name} slug={tag.slug} />
- ))}
- </div>
- )}
- <div className="feed__post-actions">
- <button type="button" className={`feed__post-action${liked ? ' feed__post-action--active' : ''}`} onClick={handleLike} aria-pressed={liked} aria-label="좋아요">
- <Heart size={18} fill={liked ? 'currentColor' : 'none'} />
- <span>{likes.toLocaleString()}</span>
- </button>
- <span className="feed__post-action feed__post-action--stat" aria-label="댓글 수">
- <MessageCircle size={18} />
- <span>{post.comments.toLocaleString()}</span>
- </span>
- <button type="button" className="feed__post-action" onClick={handleShare} aria-label="공유">
- <Share2 size={18} />
- </button>
- </div>
- </article>
- <FeedReplyComposer
- postID={post.postID}
- replyTarget={replyTarget}
- onClearReplyTarget={() => setReplyTarget(null)}
- onSubmitted={handleReplySubmitted}
- />
- <FeedReplyList
- postID={post.postID}
- initialReplies={initialReplies}
- initialTotal={initialRepliesTotal}
- onReply={handleReplyTarget}
- refreshKey={refreshKey}
- />
- {lightboxIndex !== null && (
- <FeedLightbox
- urls={imageUrls}
- startIndex={lightboxIndex}
- onClose={() => setLightboxIndex(null)}
- />
- )}
- </>
- );
- }
|